Skip to content

S01-03 Servlet-核心API ​

[TOC]

ServletConfig ​

javax.servlet.ServletConfig 是 Servlet 规范中的核心接口之一。当 Servlet 容器(如 Apache Tomcat)初始化一个 Servlet 时,会为该 Servlet 创建一个唯一的 ServletConfig 对象。

  • 一对一生命周期绑定:每个 Servlet 实例对应一个专属的 ServletConfig 对象,各个 Servlet 之间的配置相互独立、互不干扰。
  • 主要职责:向 Servlet 传递部署时的初始化参数(Init Parameters),提供当前 Servlet 的注册名称,并作为当前 Servlet 访问全局 ServletContext 的入口桥梁。

配置方式 ​

XML 配置方式 ​

在传统的 web.xml 部署描述符中,初始化参数定义在 <servlet> 标签内部的 <init-param> 子标签中:

xml
<servlet>
  <servlet-name>OrderServlet</servlet-name>
  <servlet-class>com.example.OrderServlet</servlet-class>
  <init-param>
    <param-name>defaultPageSize</param-name>
    <param-value>20</param-value>
  </init-param>
  <init-param>
    <param-name>charset</param-name>
    <param-value>UTF-8</param-value>
  </init-param>
</servlet>
<servlet-mapping>
  <servlet-name>OrderServlet</servlet-name>
  <url-pattern>/order</url-pattern>
</servlet-mapping>

注解配置方式 ​

在 Servlet 3.0 及更高规范中,可以通过 @WebServlet 注解配合 @WebInitParam 直接在类声明上配置参数:

java
// 使用注解声明 Servlet 元数据与专属初始化参数
@WebServlet(
  name = "OrderServlet",
  urlPatterns = "/order",
  // 配置当前 Servlet 专有的初始化参数列表
  initParams = {
    @WebInitParam(name = "defaultPageSize", value = "20"),
    @WebInitParam(name = "charset", value = "UTF-8")
  }
)
public class OrderServlet extends HttpServlet {}

获取与使用 ​

获取与使用 ServletConfig 主要有两种方式:

在实际开发中,自定义的 Servlet 通常继承自 HttpServlet(间接继承自 GenericServlet)。

  • 显式调用:通过 this.getServletConfig() 获取配置对象后再调用各方法。
  • 隐式委托:GenericServlet 实现了 ServletConfig 接口,可以直接在子类中使用 this.getInitParameter("key"),内部会自动委托给 ServletConfig 执行。
java
import javax.servlet.ServletConfig;
// 继承 HttpServlet,直接调用父类封装的配置方法
public class OrderServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
    // 方式一:从当前 Servlet 实例中提取配置对象
    ServletConfig config = getServletConfig();
    String pageSize = config.getInitParameter("defaultPageSize");
    ServletContext context = config.getServletContext();
  }
}

初始化流程 ​

Servlet 容器从解析配置到完成注入经历以下步骤:

  1. 解析配置:容器在启动时或首次请求到达时,扫描 web.xml 或 @WebServlet 注解,提取指定 Servlet 的元数据及 <init-param> 键值对。

  2. 实例化包装对象:容器内部为该 Servlet 创建具体的配置实例(如 Tomcat 中的 StandardWrapper 与其外观对象 StandardWrapperFacade)。

  3. 反射实例化 Servlet:容器利用反射机制调用 Servlet 的无参构造函数创建其实例。

  4. 注入配置:容器调用 Servlet.init(ServletConfig config) 方法,将装填好参数的配置对象作为实参传入。

  5. 缓存引用:GenericServlet 的 init(ServletConfig) 方法将传入的引用存入私有字段 this.config,并调用无参的 init() 供子类重写扩展。此处的 this.config 被 transient 修饰,表示它只能存在于内存中,无法被序列化,保证了它的安全性。

  6. 响应服务:后续进入 service() 及 doGet()/doPost() 处理阶段时,Servlet 实例随时读取该配置。

容器内部架构 ​

在 Servlet 容器底层实现中,为了保证安全性和隔离性,通常采用外观模式(Facade Pattern) 来包装真实配置对象。

Tomcat 内部核心控制器为 StandardWrapper,它包含了 Servlet 的完整底层运行上下文;但在向 Servlet 传递时,容器会将其包装为 StandardWrapperFacade。这样既实现了 ServletConfig 接口,又对业务代码隐藏了底层容器私有属性和敏感方法。

与 ServletContext 对比 ​

两者虽然都用于存储配置或提供环境支持,但作用域与设计意图截然不同:

维度ServletConfigServletContext
作用范围单个 Servlet 私有整个 Web 应用全局共享
实例数量多个(每个 Servlet 映射一个)唯一(每个 Web 应用只有一个)
配置来源<servlet> 下的 <init-param><context-param>
共享属性操作不支持 setAttribute支持 setAttribute / getAttribute
设计用途当前 Servlet 的专属参数(如默认分页、独立编码)全局公共配置(如全局日志、全局缓存、公共资源路径)

API: ServletConfig ​

  • String getInitParameter():(String name),读取指定初始化参数。根据参数名获取预设的字符串配置值。若指定名称不存在则返回 null。

  • Enumeration<String> getInitParameterNames():(),获取所有初始化参数名称。返回当前 Servlet 配置的所有初始化参数名称枚举集合。若未配置任何参数,则返回一个空枚举,绝不返回 null。

  • String getServletName():(),获取 Servlet 注册名称。返回在部署描述符(web.xml)中声明的 <servlet-name>,或 @WebServlet(name = ...) 中指定的逻辑名称。

  • ServletContext getServletContext():(),获取全局上下文引用。返回当前 Web 应用程序唯一的 ServletContext 实例,作为访问跨组件资源、共享属性以及容器底层能力的桥梁。

注意事项:

  1. 只读不可变设计:ServletConfig 接口未提供任何类似 setInitParameter 的修改方法。所有参数均属于启动期静态元数据,运行期间不可动态增删或修改。
  2. 类型擦除与空值防御:参数值全部以 String 形式存储。在进行数值类型转换(如 Integer.parseInt()、Boolean.parseBoolean())时,必须进行非空检查与 NumberFormatException 异常防御,避免因配置格式错误导致 Servlet 启动失败或请求中断。
  3. 与 Context 参数的界限:ServletConfig.getInitParameter() 仅能读取 <init-param> 或 @WebInitParam 声明的局部参数,无法读取 web.xml 中根节点下的 <context-param> 全局参数(后者需由 ServletContext.getInitParameter() 读取)。
  4. 构造方法调用禁区:在 Servlet 的构造方法中严禁调用 getServletConfig() 或 getServletContext()。此时容器仅完成了对象的反射实例化,尚未调用 init(ServletConfig) 进行配置注入,过早调用必将抛出 NullPointerException。
  5. init(ServletConfig) 覆写陷阱:若在自定义类中覆写了带参的 init(ServletConfig config) 方法,必须首先显式调用 super.init(config)。一旦遗漏父类调用,GenericServlet 内部的 this.config 引用将保持为 null,后续所有委派方法均会抛出空指针异常。生产环境强烈建议仅重写无参的 init() 模板方法。
java
package com.example.servlet.config.api;

import java.io.IOException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class NamingAndContextApiDemo extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    ServletConfig config = getServletConfig();

    // 1. 遍历当前配置持有的所有初始化参数名
    Enumeration<String> names = config.getInitParameterNames();
    while (names.hasMoreElements()) {
      String paramName = names.nextElement();
      // 2. 根据参数名精准读取参数取值
      String paramValue = config.getInitParameter(paramName);
      writer.println(paramName + " -> " + paramValue);
    }

    // 3. 读取当前 Servlet 的逻辑注册别名
    String servletName = config.getServletName();

    // 4. 获取所属 Web 应用的全局上下文
    ServletContext servletContext = config.getServletContext();
    String contextPath = servletContext.getContextPath();

    resp.setContentType("text/plain;charset=UTF-8");
    resp.getWriter().write(String.format("Servlet 注册别名: %s, 归属上下文路径: %s", servletName, contextPath));
  }
}

ServletContext ​

javax.servlet.ServletContext 是 Java Servlet 规范中定义的全局接口,代表当前 Web 应用程序的运行上下文环境。

  • 单一实例:每一个部署到 Servlet 容器(如 Apache Tomcat)中的 Web 应用,在同一个 JVM 内只对应一个唯一的 ServletContext 实例。
  • 全局共享:作为应用级别的顶级容器,当前 Web 应用内的所有 Servlet、Filter、Listener 均可访问同一个 ServletContext。
  • 环境门面:它不仅负责在不同组件间共享数据,还充当了业务代码与底质容器通信的标准化入口。

生命周期 ​

ServletContext 对象的存在周期与 Web 应用程序的生命周期完全一致:

  1. 容器启动与解析:Servlet 容器启动并加载 Web 应用程序时,解析 web.xml 或相关注解配置。

  2. 上下文初始化:容器实例化该应用的 ServletContext 对象,并读取全局初始化参数。

  3. 通知监听器:容器触发 ServletContextListener.contextInitialized() 事件,执行系统启动任务(如框架初始化、数据库连接池预热)。

  4. 服务运行期:在整个应用对外提供服务期间,该对象持续存活于内存中供各组件读取和修改。

  5. 容器停止与清理:Web 应用被卸载或容器优雅停机时,容器触发 ServletContextListener.contextDestroyed() 方法执行资源释放。

  6. 对象销毁:监听器执行完毕后,容器销毁 ServletContext 实例并交由垃圾回收器回收。

获取途径 ​

在不同的组件与处理阶段中,有多种途径可以获取到 ServletContext 实例:

java
// 途径一:直接通过继承的 HttpServlet 获取
ServletContext ctx1 = getServletContext();

// 途径二:通过当前请求对象获取
ServletContext ctx2 = request.getServletContext();

// 途径三:通过会话对象获取
ServletContext ctx3 = session.getServletContext();
  • 基于 ServletConfig:通过 servletConfig.getServletContext() 获取(GenericServlet 已对其进行封装,子类可直接调用 getServletContext())。
  • 基于 HttpServletRequest:Servlet 3.0 新增了 request.getServletContext(),在未持有 Servlet 实例的工具类中更为便捷。
  • 基于 HttpSession:调用 session.getServletContext()。
  • 基于 FilterConfig:在过滤器中通过 filterConfig.getServletContext() 获取。
  • 基于 ServletContextEvent:在应用级监听器中通过 event.getServletContext() 获取。

全局参数 ​

全局初始化参数针对整个 Web 应用生效,通常用于声明与业务组件解耦的全局配置(如外部配置文件路径、全局调试开关)。

在 web.xml 中使用 <context-param> 标签进行配置:

xml
<context-param>
  <param-name>globalConfigPath</param-name>
  <param-value>/WEB-INF/system-config.properties</param-value>
</context-param>

在代码中通过以下方法读取:

  • getInitParameter(String name):根据参数键名获取对应的字符串值;若键不存在则返回 null。
  • getInitParameterNames():获取包含所有全局初始化参数名称的 Enumeration<String>。

域对象操作 ​

ServletContext 是 Servlet 规范中生命周期最长、作用域最大的域对象(Application Scope)。

java
// 向应用域中写入全局共享数据
context.setAttribute("globalVisitorCount", 100);

// 从应用域中读取指定键名的共享数据
Object count = context.getAttribute("globalVisitorCount");

// 从应用域中显式移除不再需要的键值数据
context.removeAttribute("globalVisitorCount");
  • 线程安全提示:由于所有客户端请求线程共享同一个 ServletContext,多线程并发读写存入的属性时必须采取并发同步策略(例如使用 AtomicInteger、ConcurrentHashMap 或显式锁机制),否则会引发数据不一致问题。
  • 内存占用考量:存入 ServletContext 的对象在整个 Web 应用生命周期内不会被自动回收,应避免在此存放大量或高频变动的数据,以防止内存溢出。

资源读取 ​

Web 应用打包为 WAR 包后,传统的相对路径无法直接对应操作系统底层的文件路径。ServletContext 提供了统一的资源定位与读取支持:

java
// 获取 Web 应用根目录下资源的绝对磁盘物理路径
String realPath = context.getRealPath("/WEB-INF/config.properties");

// 直接以输入流的方式读取应用根目录下的静态资源
InputStream in = context.getResourceAsStream("/WEB-INF/db.properties");

// 根据资源文件后缀名获取对应的标准 MIME 类型
String mimeType = context.getMimeType("report.pdf");
  • getRealPath(String path):将相对于 Web 应用根目录的虚拟路径转换为服务器本地磁盘的真实物理绝对路径。需要注意,若 WAR 包未解压直接在内存中运行,该方法可能返回 null。
  • getResourceAsStream(String path):以 Web 根目录为基准直接打开输入流,路径必须以 / 开头。此方式在打包部署环境下具备最佳的移植性。
  • getMimeType(String file):解析文件扩展名对应的互联网媒体类型(如传入 logo.png 返回 image/png)。

动态注册 ​

从 Servlet 3.0 规范开始,ServletContext 支持在 Web 应用初始化阶段动态注册三大核心组件,这使得各类微框架无需在 web.xml 中编写繁复的静态标签:

  • addServlet(String servletName, Class<? extends Servlet> servletClass):动态注册 Servlet,并返回 ServletRegistration.Dynamic 对象以进一步绑定 URL 映射和初始化参数。
  • addFilter(String filterName, Class<? extends Filter> filterClass):动态注册 Filter 并指定拦截规则。
  • addListener(Class<? extends EventListener> listenerClass):动态注册监听器。

需要注意,动态注册方法仅允许在应用初始化期间(即 ServletContextListener.contextInitialized 执行期间或 ServletContainerInitializer.onStartup 中)调用,应用初始化完成一旦对外提供服务,该操作将被容器严格锁定。

容器底层实现 ​

在 Servlet 容器底层(以 Apache Tomcat 为例),ServletContext 的实现采用了标准的外观模式(Facade Pattern)。

image-20260908162754885

如上图所示,在 Tomcat 的层级架构中:

  • Context(实现类为 StandardContext):对应图中的 Context 层级,表示一个运行中的 Web 应用,内部管理着多个 Wrapper(即各个具体的 Servlet)。
  • ApplicationContext:StandardContext 内部维护的核心上下文对象,承载容器大部分具体逻辑。
  • ApplicationContextFacade:为了防止开发者编写的业务代码直接将接口强制转型为 StandardContext 并调用底层敏感的管理方法(如卸载应用、停止服务),Tomcat 向用户暴露的是 ApplicationContextFacade 外观类,所有对外调用的 API 最终透传给内部的 ApplicationContext,起到了严格的安全防护与解耦作用。

四大域对象对比 ​

Servlet 规范中定义了四大作用域对象,其生存周期与可见范围逐级递增:

域对象对应接口生命周期共享范围
Page ScopePageContext当前 JSP 页面解析执行期间单个页面内部
Request ScopeHttpServletRequest一次 HTTP 请求从到达至响应完毕同一请求链条(含请求转发)
Session ScopeHttpSession客户端会话建立至超时失效或注销同一客户端多次请求之间
Application ScopeServletContextWeb 应用加载启动至容器停止卸载整个 Web 应用的所有用户与组件

API: ServletContext ​

上下文元数据与全局配置 ​

  • String getContextPath():(),获取应用上下文路径。返回当前 Web 应用程序挂载的基础 URI 路径(以 / 开头,根上下文返回空字符串 "")。

  • String getInitParameter():(String name),读取全局初始化参数。根据参数键名获取整个 Web 应用上下文共享的静态配置字符串。

  • Enumeration<String> getInitParameterNames():(),获取所有全局参数名。返回当前应用中配置的所有全局初始化参数名称的枚举集合。

  • boolean setInitParameter():(String name, String value),动态写入全局参数。向应用上下文添加全局配置项。仅允许在上下文初始化阶段调用。

  • int getMajorVersion():(),获取 Servlet 规范主版本号。返回当前 Web 容器支持的 Servlet 规范主版本(例如 Servlet 3.1 返回 3)。

  • int getMinorVersion():(),获取 Servlet 规范次版本号。返回当前 Web 容器支持的 Servlet 规范次版本(例如 Servlet 3.1 返回 1)。

  • String getServerInfo():(),获取容器环境标识。返回当前 Servlet 容器的软件名称和版本信息(例如 Apache Tomcat/9.0.65)。

  • String getServletContextName():(),获取显示名称。返回在 web.xml 的 <display-name> 标签中定义的应用程序描述名称。

注意事项:

  1. setInitParameter 时机锁定:setInitParameter(name, value) 仅能在 ServletContextListener.contextInitialized 执行阶段或 Servlet 容器初始化入口中调用。一旦整个应用完成初始化并对外暴露 HTTP 服务,该方法将直接返回 false 且不生效。
  2. getContextPath 结尾斜杠:该方法返回的路径绝不以 / 结尾(除非当前应用本身即为根上下文 /,此时返回空字符串 "")。在进行 URI 拼接时,开发者必须自行补齐前导斜杠(例如 context.getContextPath() + "/api/v1")。
java
package com.example.servlet.context.api;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ContextMetadataApiDemo extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    ServletContext context = getServletContext();

    // 1. 读取上下文挂载与容器环境元数据
    String contextPath = context.getContextPath();
    String serverInfo = context.getServerInfo();
    int major = context.getMajorVersion();
    int minor = context.getMinorVersion();
    String appName = context.getServletContextName();

    // 2. 尝试向运行时写入全局参数(此时已过初始化期,应返回 false)
    boolean writeSuccess = context.setInitParameter("runtimeKey", "runtimeValue");

    resp.setContentType("text/plain;charset=UTF-8");
    PrintWriter out = resp.getWriter();
    out.println("上下文路径: " + contextPath);
    out.println("容器版本: " + serverInfo);
    out.println("Servlet 规范版本: " + major + "." + minor);
    out.println("应用注册名: " + appName);
    out.println("运行时写入参数结果: " + writeSuccess);

    // 3. 遍历全局配置参数
    out.println("--- 全局参数列表 ---");
    Enumeration<String> params = context.getInitParameterNames();
    while (params.hasMoreElements()) {
      String paramKey = params.nextElement();
      out.println(paramKey + " = " + context.getInitParameter(paramKey));
    }
  }
}

全局属性共享与状态管理 ​

  • Object getAttribute():(String name),读取全局共享属性。根据指定键名获取存储在 Application 作用域下的对象引用。若属性不存在则返回 null。

  • Enumeration<String> getAttributeNames():(),获取所有属性名称。返回当前上下文中绑定的所有全局属性键名集合。

  • void setAttribute():(String name, Object object),设置全局共享属性。以键值对形式绑定对象到 Application 作用域。若传入的对象为 null,等效于调用 removeAttribute。

  • void removeAttribute():(String name),移除全局共享属性。从 Application 作用域中根据键名彻底清除绑定的对象。

注意事项:

  1. 内存泄漏规避:存入 ServletContext 的对象其生命周期与 JVM 中的 Web 应用等长。严禁在全局属性中存放生命周期短暂的大对象(如超大请求体 ByteArray、未关闭的连接池引用),否则这些对象将长期驻留老年代,导致持续 Full GC 甚至引发 OutOfMemoryError。
  2. 监听器事件级联触发:每次调用 setAttribute、removeAttribute 时,容器会同步阻塞触发已注册的 ServletContextAttributeListener 监听器。若监听器回调中存在重度计算或网络 IO,将直接拖慢调用方的工作线程响应速度。
java
package com.example.servlet.context.api;

import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ContextAttributeApiDemo extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    ServletContext context = getServletContext();

    // 1. 存入全局属性
    context.setAttribute("SERVICE_STATUS", "ONLINE");
    context.setAttribute("NODE_ID", "NODE-SH-01");

    // 2. 读取全局属性
    String serviceStatus = (String) context.getAttribute("SERVICE_STATUS");

    // 3. 遍历所有全局属性键名
    Enumeration<String> attributeNames = context.getAttributeNames();
    StringBuilder namesBuilder = new StringBuilder();
    while (attributeNames.hasMoreElements()) {
      namesBuilder.append(attributeNames.nextElement()).append(", ");
    }

    // 4. 清理指定属性
    context.removeAttribute("NODE_ID");

    resp.setContentType("text/plain;charset=UTF-8");
    resp.getWriter().write(String.format("服务状态: %s\n全量属性键: %s\nNODE_ID 移除后校验: %s",
        serviceStatus, namesBuilder.toString(), context.getAttribute("NODE_ID")));
  }
}

物理路径解析与资源读取 ​

  • String getRealPath():(String path),虚拟路径转操作系统物理路径。将基于 Web 根目录的虚拟文件系统路径解析为绝对文件系统路径。

  • InputStream getResourceAsStream():(String path),以输入流读取静态资源。以流的方式直接读取指定相对路径的应用内部资源。

  • URL getResource():(String path),获取资源的统一资源定位符。返回指向目标资源的 java.net.URL 对象,用于进一步提取 URL 连接元数据。

  • String getMimeType():(String file),解析文件的 MIME 类型。根据文件名后缀匹配容器配置的媒体传输格式(例如输入 avatar.png 返回 image/png)。

注意事项:

  1. 前导斜杠硬性要求:调用 getRealPath(path)、getResourceAsStream(path) 与 getResource(path) 时,传入的路径参数必须严格以 / 开头,代表当前 Web 应用程序的根路径(WebRoot)。若省略开头的斜杠,容器将抛出 MalformedURLException 或静默返回 null。
  2. 安全隔离目录访问:/WEB-INF/ 与 /META-INF/ 目录虽然受到容器保护禁止客户端通过浏览器直接发起 HTTP 访问,但通过 ServletContext.getResourceAsStream("/WEB-INF/...") 是完全被允许且安全的,是存放私有系统配置与敏感依赖的标准位置。
java
package com.example.servlet.context.api;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ResourceResolutionApiDemo extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    ServletContext context = getServletContext();

    // 1. 物理路径推导
    String realPath = context.getRealPath("/index.html");

    // 2. URL 对象解析
    URL resourceUrl = context.getResource("/WEB-INF/web.xml");

    // 3. 安全的数据流获取
    InputStream stream = context.getResourceAsStream("/WEB-INF/web.xml");
    boolean streamAvailable = (stream != null);
    if (stream != null) {
      stream.close();
    }

    // 4. MIME 媒体类型推断
    String htmlMime = context.getMimeType("sample.html");
    String jsonMime = context.getMimeType("response.json");
    String pdfMime = context.getMimeType("document.pdf");

    resp.setContentType("text/plain;charset=UTF-8");
    resp.getWriter().write(String.format(
      "物理路径: %s\n资源 URL: %s\n流可用性: %b\nHTML MIME: %s\nJSON MIME: %s\nPDF MIME: %s",
      realPath, resourceUrl, streamAvailable, htmlMime, jsonMime, pdfMime
    ));
  }
}

请求转发与系统日志 ​

  • RequestDispatcher getRequestDispatcher():(String path),路径请求转发器。根据指定的相对或绝对路径创建转发调度器。

  • RequestDispatcher getNamedDispatcher():(String name),命名组件请求转发器。根据部署时声明的 Servlet 逻辑别名定位并创建转发调度器。

  • void log():(String msg),写入容器日志。向 Web 容器专有的应用程序日志文件中输出普通文本信息。

  • void log():(String message, Throwable throwable),写入异常日志。向 Web 容器日志输出描述信息以及附带的异常堆栈跟踪。

注意事项:

  1. 路径相对性与 ServletRequest 的差异:ServletContext.getRequestDispatcher(path) 中的路径必须以 / 开头,从当前 Web 应用程序的根路径起算;而 ServletRequest.getRequestDispatcher(path) 允许使用不带斜杠的相对路径(相对于当前 Servlet 的 URL 映射)。
  2. 日志重定向问题:context.log(...) 的底层实现依赖宿主容器自身的日志配置(如 Tomcat 的 catalina.out 或 localhost.log),它不支持现代日志框架(SLF4J、Logback)的高级路由、日志切割与异步队列,仅推荐用于容器生命周期底层的故障诊断排查。
java
package com.example.servlet.context.api;

import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class DispatchAndLoggingApiDemo extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    ServletContext context = getServletContext();

    // 1. 通过系统日志打印诊断信息
    context.log("[DispatchDemo] 开始执行系统分发链路调度");

    try {
      // 2. 基于绝对虚拟路径获取转发器
      RequestDispatcher pathDispatcher = context.getRequestDispatcher("/target-path");

      // 3. 基于 Servlet 命名别名获取转发器
      RequestDispatcher namedDispatcher = context.getNamedDispatcher("TargetComponentServlet");

      // 4. 模拟条件转发
      if (req.getParameter("useNamed") != null && namedDispatcher != null) {
        namedDispatcher.forward(req, resp);
      } else if (pathDispatcher != null) {
        pathDispatcher.forward(req, resp);
      }
    } catch (Exception ex) {
      // 5. 记录带异常堆栈的错误日志
      context.log("[DispatchDemo] 分发调用链异常断裂", ex);
      resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "分发路由失败");
    }
  }
}

动态组件注册 ​

  • ServletRegistration.Dynamic addServlet():(String servletName, Class<? extends Servlet> servletClass),动态注册 Servlet。在运行期通过类类型向容器中注册全新的 Servlet 组件并返回可配置实体。

  • FilterRegistration.Dynamic addFilter():(String filterName, Class<? extends Filter> filterClass),动态注册 Filter。在运行期通过类类型向应用上下文中注册过滤器组件。

  • void addListener():(Class<? extends EventListener> listenerClass),动态注册监听器。在运行期向应用注册事件监听器(如生命周期监听器、会话监听器)。

  • SessionCookieConfig getSessionCookieConfig():(),获取会话 Cookie 配置对象。返回用于自定义管理 JSESSIONID Cookie 属性(如 HttpOnly、Secure、Path、Domain)的配置门面。

注意事项:

  1. 注册窗口期锁闭:所有 addServlet、addFilter 和 addListener 动态配置接口,仅被允许在应用启动阶段(即 ServletContextListener.contextInitialized 执行期间或 ServletContainerInitializer.onStartup 方法中)被调用。一旦容器完全就绪开始接收请求,在 doGet、doPost 等服务方法中调用动态注册接口将直接抛出 IllegalStateException。
  2. 类加载器一致性:使用 Class 或全类名注册组件时,必须确保传入的类型能被当前 Web 应用专有的类加载器(如 Tomcat 的 WebappClassLoader)正确加载,避免因双亲委派机制冲突导致类转换失败。
java
package com.example.servlet.context.api;

import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletRegistration;
import javax.servlet.SessionCookieConfig;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServlet;

@WebListener
public class DynamicRegistrationApiDemo implements ServletContextListener {

  @Override
  public void contextInitialized(ServletContextEvent sce) {
    ServletContext context = sce.getServletContext();

    // 1. 动态挂载 Servlet 组件
    ServletRegistration.Dynamic dynamicServlet =
        context.addServlet("DynamicApiServlet", HttpServlet.class);
    if (dynamicServlet != null) {
      dynamicServlet.addMapping("/dynamic-api/*");
      dynamicServlet.setLoadOnStartup(2);
      dynamicServlet.setAsyncSupported(true);
    }

    // 2. 动态挂载 Filter 过滤器
    FilterRegistration.Dynamic dynamicFilter =
        context.addFilter("DynamicSecurityFilter", Filter.class);
    if (dynamicFilter != null) {
      dynamicFilter.addMappingForUrlPatterns(
          EnumSet.of(DispatcherType.REQUEST), true, "/*");
    }

    // 3. 动态配置全局会话 Cookie 增强安全性
    SessionCookieConfig cookieConfig = context.getSessionCookieConfig();
    cookieConfig.setHttpOnly(true);
    cookieConfig.setSecure(false);
    cookieConfig.setName("SECURE_JSESSIONID");

    // 4. 动态添加自定义事件监听器
    context.addListener(DynamicRegistrationApiDemo.class);
  }

  @Override
  public void contextDestroyed(ServletContextEvent sce) {
    // 上下文销毁钩子
  }
}

实战:网站计数器 ​

设计原理

在韩顺平老师的 JavaWeb 教学体系中,ServletContext 常被形象地比作“教室前方的大黑板”:

  • 黑板效应:只要 Web 应用程序不重启,这块黑板就始终存在;任何一位同学(客户端浏览器)走进来,都能看到黑板上的内容,也可以在上面修改数字。
  • 作用域差异:HttpServletRequest 相当于单次传递的纸条,HttpSession 相当于每个学生独立的储物柜,而 ServletContext 则是全校师生公共共享的全局空间。
  • 核心机制:利用该对象在整个应用生命周期中全局唯一且所有用户共享的特性,将访问次数作为属性存储在其中。

image-20260908162832584

代码实现

仅使用单个 Servlet 类实现访问计数,通过重写 doGet 方法完成数据的读取、累加与回写:

java
package com.hspedu.servlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/visit")
public class VisitCountServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    // 1. 设置响应格式与编码,防止输出中文乱码
    resp.setContentType("text/html;charset=utf-8");
    PrintWriter out = resp.getWriter();

    // 2. 获取当前 Web 应用全局唯一的 ServletContext 对象
    ServletContext servletContext = getServletContext();

    // 3. 从 ServletContext 中读取名为 visitCount 的全局属性
    Object visitCount = servletContext.getAttribute("visitCount");

    // 4. 判断是否首次访问:若无记录则初始化为1,已有记录则递增
    if (visitCount == null) {
      servletContext.setAttribute("visitCount", 1);
      visitCount = 1;
    } else {
      int count = Integer.parseInt(visitCount.toString()) + 1;
      servletContext.setAttribute("visitCount", count);
      visitCount = count;
    }

    // 5. 将当前统计值响应回浏览器客户端
    out.println("<h1>网站访问统计</h1>");
    out.println("<p>当前网站累计访问量为: " + visitCount + "</p>");
  }

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    doGet(req, resp);
  }
}

执行流程

  1. 用户在浏览器地址栏输入 http://localhost:8080/项目名/visit 发起请求。

  2. Tomcat 容器接收请求,匹配并调用 VisitCountServlet 的 doGet 方法。

  3. 执行 getServletContext() 方法,取得当前 Web 应用的全局上下文。

  4. 调用 getAttribute("visitCount") 检查上下文域中是否已存储计数器。

  5. 若返回值为 null,说明是 Web 应用启动后的首次访问,设置计数值为 1 并写入上下文。

  6. 若返回值不为 null,将取出的计数值转换为整型并加 1,随后调用 setAttribute 覆盖旧值。

  7. 组装 HTML 文本,通过字符输出流将最新的统计结果输出至浏览器展示。


线程安全

在多用户高并发同时访问时,ServletContext 中的属性读写可能出现并发竞争问题(即读取与写回非原子操作导致计数丢失)。

可以通过将 servletContext 作为锁对象进行同步保护:

java
ServletContext servletContext = getServletContext();
Object visitCount;

// 使用当前唯一的全局上下文对象加锁保证并发安全
synchronized (servletContext) {
  visitCount = servletContext.getAttribute("visitCount");
  if (visitCount == null) {
    servletContext.setAttribute("visitCount", 1);
    visitCount = 1;
  } else {
    int count = Integer.parseInt(visitCount.toString()) + 1;
    servletContext.setAttribute("visitCount", count);
    visitCount = count;
  }
}
  • 加锁对象选择:必须锁定唯一的全局对象(如 servletContext),不可使用 this,因为不同请求如果被不同实例处理则无法形成互斥。

HttpServletRequest ​

核心定位 ​

javax.servlet.http.HttpServletRequest 是 Java Web 开发中最基础的核心接口之一。当客户端浏览器向 Servlet 容器(如 Apache Tomcat)发送一条 HTTP 请求时,容器负责解析底层的 TCP 字节流,并将其完整封装为一个实现了 HttpServletRequest 接口的对象。

  • 数据载体:封装了客户端发来的完整 HTTP 请求报文,包含请求行、请求头、空行和请求体。
  • 通信接口:作为开发者与客户端请求交互的标准入口,提供统一的 API 读取参数、识别客户端环境、管理单次请求生命周期内的数据流转。

报文映射 ​

HTTP 协议基于纯文本传输,HttpServletRequest 的各类 API 与底层的 HTTP 报文结构有着严格的映射关系。

  • 请求行:包含请求方法、请求路径(URL)和 HTTP 协议版本,对应 getMethod()、getRequestURI() 等方法。
  • 首部行(请求头):包含键值对形式的元数据(如 Host、User-Agent、Cookie),对应 getHeader() 等方法。
  • 空行:作为协议标准的分隔符,用于标记首部结束与请求体开始。
  • 实体主体(请求体):承载 POST、PUT 等请求携带的表单数据或 JSON 负载,对应 getParameter()、getInputStream() 等方法。

image-20260909105555738

继承体系 ​

HttpServletRequest 并不是孤立存在的接口,它处于 Servlet 规范的继承与实现体系中:

  • ServletRequest:顶层通用接口,定义了与具体应用层协议无关的基础方法(如获取参数、字符编码、域属性操作)。
  • HttpServletRequest:继承自 ServletRequest,专门针对 HTTP/HTTPS 协议进行扩展,补充了请求行、请求头、Cookie、Session 及请求转发等 HTTP 专用 API。
  • 容器实现类:在 Tomcat 内部,实际运行的对象为 org.apache.catalina.connector.RequestFacade(外观模式实现),它包装了底层的 Request 实体,防止业务代码触碰容器内部的管理方法。

image-20260909110123190

生命周期 ​

HttpServletRequest 是典型的请求级别对象,生命周期短暂且与单次 HTTP 请求强绑定:

  1. 建立连接与解析:客户端发起 HTTP 请求,Tomcat 连接器(Connector)捕获连接,读取网络流并解析 HTTP 协议格式。

  2. 实例化对象:容器创建 HttpServletRequest 与 HttpServletResponse 实例。

  3. 路由分发:容器根据 URL 映射规则定位到目标 Servlet,调用其 service() 方法,并将 request 与 response 对象作为参数传入。

  4. 业务处理:Servlet 读取 request 中的数据并执行业务逻辑(如校验、查询、转发)。

  5. 响应完成与销毁:服务端响应返回给客户端后,该次 HTTP 请求结束,容器立刻将 request 对象标记为失效并销毁(或回收至对象池),内存由 GC 清理。

请求转发 ​

请求转发(Request Forwarding) 是 Servlet 规范提供的一种服务器端内部资源调度机制。

当客户端向服务器发送请求后,当前 Servlet 并不直接生成最终响应,而是通过服务器内部的调度器,将请求对象(HttpServletRequest)和响应对象(HttpServletResponse)原封不动地传递给另一个资源(可以是另一个 Servlet、JSP 或静态 HTML 页面),由目标资源继续处理并负责向浏览器输出结果。

整个流转过程完全在服务器内部封闭进行,客户端浏览器对此毫无感知。

运作机制 ​

请求转发最显著的特征是单次 HTTP 请求闭环。客户端只与服务端建立一次网络连接,浏览器地址栏保持初次请求的 URL 不变。

image-20260908215235221

如上图所示,整体执行包含四个核心阶段:

  1. 客户端发起请求:用户浏览器向 Servlet1 发起 HTTP 请求。

  2. 服务器内部转发:Servlet1 调用 forward() 方法,把请求和响应交由 Servlet2 承接。

  3. 目标资源处理:Servlet2 读取请求上下文并生成最终的响应实体。

  4. 统一回写响应:由 Web 容器将响应体直接返回给客户端浏览器。

执行流程:

image-20260908215630105

获取调度器方式 ​

  • 通过 request.getRequestDispatcher(String path) 获取,路径支持相对路径或以 / 开头的绝对路径。
  • 通过 servletContext.getRequestDispatcher(String path) 获取,路径必须以 / 开头(相对于当前 Web 应用根目录)。

API: RequestDispatcher ​

  • void forward():(ServletRequest request, ServletResponse response),服务端请求转发。将请求从当前 Servlet 移交给同一应用中的另一资源(Servlet、JSP 或静态资源),交出最终响应控制权。

  • void include():(ServletRequest request, ServletResponse response),服务端内容包含。将目标资源的响应输出内容嵌套、并入到当前 Servlet 的输出流中,保留自身控制权。

注意事项:

  1. 响应提交后转发异常:必须在调用 forward() 之前使用 response.isCommitted() 进行安全前置探测。一旦底层缓冲区已向网络连接发送数据,触发 forward() 将抛出 IllegalStateException。
  2. 转发后代码穿透陷阱:forward() 与 include() 是同步方法调用,并不是 return 语句。当目标资源执行完毕后,执行流程仍会返回到 forward() 调用的下一行代码继续向下运行。如果在 forward() 后没有紧随 return;,后续代码修改变量或抛出异常将产生不可预期的系统副作用。
  3. 死循环转发规避:严禁在资源链中构建无终结条件的循环转发(例如组件 A forward 至组件 B,组件 B 又 forward 至组件 A),这会导致容器的工作线程栈空间迅速耗尽,直接抛出 StackOverflowError。
java
package com.example.servlet.dispatcher.api;

import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CoreDispatcherApiDemo extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {

    String action = req.getParameter("action");

    if ("includeDemo".equals(action)) {
      RequestDispatcher dispatcher = req.getRequestDispatcher("/fragments/nav-bar.html");
      if (dispatcher != null) {
        // 1. 将外部静态模块片段包含到当前响应中
        dispatcher.include(req, resp);
      }
      return;
    }

    RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/dashboard.jsp");
    if (dispatcher != null) {
      // 2. 防御性检查是否已经提交响应
      if (!resp.isCommitted()) {
        // 执行请求转发并彻底交出响应流控制权
        dispatcher.forward(req, resp);
        // 必须紧跟 return,防止后续代码发生穿透执行
        return;
      }
    }

    resp.sendError(HttpServletResponse.SC_NOT_FOUND, "目标分发视图不可用");
  }
}

代码实现 ​

编写两个 Servlet,演示由 SourceServlet 处理业务并向 TargetServlet 转发请求:

java
package com.example.servlet;

import javax.servlet.RequestDispatcher;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/source")
public class SourceServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // 1. 向当前请求域存放业务共享数据
    req.setAttribute("role", "admin");
    // 2. 获取请求调度器并指定目标资源路径
    RequestDispatcher dispatcher = req.getRequestDispatcher("/target");
    // 3. 执行转发将当前请求与响应流转至目标组件
    dispatcher.forward(req, resp);
  }
}

目标组件接收请求并提取数据:

java
// 从请求域提取由前置 Servlet 存放的共享数据
Object role = req.getAttribute("role");
// 输出响应内容交付客户端
resp.getWriter().println("Target received role: " + role);

数据共享 ​

请求转发之所以能够在多个组件间无缝传递数据,核心原因在于作用域对象的共享:

  • 对象同一性:在整条转发链路中,从起点组件到终点组件,操作的都是容器初始创建的同一个 HttpServletRequest 内存实例。
  • 数据流转通道:通过 req.setAttribute("key", value) 设值,目标组件通过 req.getAttribute("key") 取值,只要单次请求未结束,数据便始终有效。
  • 自动垃圾回收:当目标资源渲染完毕并最终响应给客户端后,该次 HTTP 请求彻底结束,request 对象被容器回收,域中存储的临时数据随之销毁,不会造成全局内存泄露。

路径规范 ​

在编写转发路径时,路径格式与访问权限具有严格的规则:

  • 斜杠开头(绝对路径):形如 req.getRequestDispatcher("/user/list"),此处的斜杠 / 代表当前 Web 应用程序的根路径(即 http://localhost:8080/工程名/),无需也不可手动拼接项目名(Context Path)。
  • 非斜杠开头(相对路径):形如 req.getRequestDispatcher("detail"),相对于当前发起转发的 Servlet 所在的 URI 路径进行相对寻址。
  • 越权保护能力:Web 应用的 /WEB-INF/ 目录属于受保护资源,浏览器直接在地址栏输入 URL 访问会被服务器直接拦截并返回 404;但通过服务端的 forward() 方法可以合法且安全地访问 /WEB-INF/ 内部的 JSP 或 HTML 文件,常用于防止用户绕过认证直接访问受保护页面。
  • 局限性:请求转发完全依托于当前 Web 容器环境,因此无法转发到外部站点(如无法转发到 [http://www.baidu.com](http://www.baidu.com))。
  • 重复支付问题:因为浏览器地址栏会停止在第一个servlet ,如果你刷新页面, 会再次发出请求(并且会带数据),所以在支付页面情况下, 不要使用请求转发, 否则会造成重复支付。

转发与重定向 ​

请求转发(Forward)与客户端重定向(Redirect)是 Web 开发中极易混淆的两种页面跳转方式:

比较维度请求转发(Forward)客户端重定向(Redirect)
跳转位置服务器端内部调度流转客户端浏览器重新发起连接
HTTP 请求次数仅 1 次网络请求至少 2 次网络请求
浏览器地址栏保持初始请求 URL 不变变为最终重定向的新 URL
数据共享性共享同一个 request 域数据产生新请求,原 request 域失效
目标资源范围仅限当前 Web 应用内部资源可以是当前项目或任意外部 URL
受保护目录能够直接访问 /WEB-INF/ 资源无法直接访问 /WEB-INF/ 资源
核心 APIrequest.getRequestDispatcher().forward()response.sendRedirect()

乱码处理 ​

由于客户端编码与服务端解码采用的字符集可能存在差异,需要针对性地处理中文乱码:

java
// 针对 POST 请求体中的字节流指定服务端统一解码编码
req.setCharacterEncoding("UTF-8");
// 仅需在首次调用读取参数前设定一次即可生效
String username = req.getParameter("username");
  • POST 请求乱码:在读取任何参数(如调用 getParameter())之前,必须先调用 req.setCharacterEncoding("UTF-8")。
  • GET 请求乱码:GET 参数位于请求行 URL 中,编码由容器的连接器决定。Tomcat 8.0 及以上版本默认使用 UTF-8 对 URI 进行解码,通常无需手动配置;旧版本需在 server.xml 的 <Connector> 标签中显式配置 URIEncoding="UTF-8"。

API: HttpServletRequest ​

HttpServletRequest 是纯接口,其实例由容器运行时注入;对于装饰器扩展场景,标准库提供了 HttpServletRequestWrapper 包装类构造方法。

构造方法 ​

  • HttpServletRequestWrapper HttpServletRequestWrapper():(HttpServletRequest request),包装器构造方法。基于装饰器模式构建一个适配包装对象,用于安全拦截并重写请求处理逻辑(如重写输入流、重定向参数等)。

注意事项:

  1. 传入的被包装对象 request 不得为 null,否则抛出 IllegalArgumentException。
  2. HttpServletRequestWrapper 默认实现将所有方法调用原封不动地透传给底层被包装的实例,开发者仅需针对性重写需要变更逻辑的目标方法。
java
package com.example.servlet.request.api;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class ConstructorApiDemo {
  public void demonstrateWrapperCreation(HttpServletRequest sourceRequest) {
    // 创建通用的 HTTP 请求包装器实例
    HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(sourceRequest) {
      @Override
      public String getHeader(String name) {
        if ("X-Custom-Auth".equalsIgnoreCase(name)) {
          return "Injected-Token-Value";
        }
        return super.getHeader(name);
      }
    };
  }
}

请求行 ​

  • String getMethod():(),获取 HTTP 请求动词。返回客户端请求所使用的标准 HTTP 动词名称(如 GET、POST、PUT、DELETE)。

  • String getRequestURI():(),获取请求统一资源标识符。返回从协议主机名后开始到查询字符串(QueryString)之前的相对路径(包含 ContextPath)。

  • StringBuffer getRequestURL():(),获取请求完整资源定位符。返回包含协议、主机名、端口号与 URI 路径的完整 URL 构建器,不包含查询字符串。

  • String getContextPath():(),获取应用上下文挂载路径。返回当前 Web 应用程序挂载的基础路径(根上下文返回空字符串 "")。

  • String getServletPath():(),获取当前 Servlet 映射路径。返回触发当前 Servlet 调用的匹配 URL 规则路径部分。

  • String getPathInfo():(),获取额外路径信息。返回在 ServletPath 之后、QueryString 之前的附加路径。若无通配匹配则返回 null。

  • String getQueryString():(),获取原始查询字符串。返回 URL 中 ? 之后的原始查询串,未经过 URL 解码。若无参数则返回 null。

  • String getProtocol():(),获取协议版本。返回请求使用的网络协议名称及版本(如 HTTP/1.1 或 HTTP/2.0)。

注意事项:

  1. getRequestURL() 返回类型的非易失性:该方法返回的是 StringBuffer 而非 String,主要是 Servlet 早期规范的历史遗留设计。由于其未实现不可变性,严禁将其直接共享给多线程修改。
  2. getRequestURI() 防范路径穿越:getRequestURI() 返回的是未经规范化的原始路径,如果请求中包含 ..、// 等特殊符号,在进行权限认证或安全路径比对前,必须使用安全工具(如 Spring 的 UrlPathHelper 或 Java 原生 Path.normalize())进行路径归一化,防止路径遍历越权漏洞。
java
package com.example.servlet.request.api;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RequestLineApiDemo extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    // 1. 提取 HTTP 请求动词与传输协议
    String method = req.getMethod();
    String protocol = req.getProtocol();

    // 2. 提取路径层次与定位符
    String uri = req.getRequestURI();
    StringBuffer urlBuffer = req.getRequestURL();
    String contextPath = req.getContextPath();
    String servletPath = req.getServletPath();
    String pathInfo = req.getPathInfo();
    String queryString = req.getQueryString();

    resp.setContentType("text/plain;charset=UTF-8");
    resp.getWriter().write(String.format(
      "Method: %s\nProtocol: %s\nURI: %s\nURL: %s\nContext: %s\nServlet: %s\nPathInfo: %s\nQuery: %s",
      method, protocol, uri, urlBuffer.toString(), contextPath, servletPath, pathInfo, queryString
    ));
  }
}

请求头 ​

  • String getHeader():(String name),读取指定单值请求头。返回指定 Header 名称的字符串取值。若 Header 不存在则返回 null。

  • Enumeration<String> getHeaders():(String name),读取同名多值请求头。返回指定 Header 名称所关联的所有值的枚举集合(例如包含多个 Accept 或 Cookie)。

  • Enumeration<String> getHeaderNames():(),获取所有请求头名称。返回当前请求报文中携带的所有 Header 键名的枚举集合。

  • int getIntHeader():(String name),读取整型请求头。将指定请求头的值直接转换为 int。若不存在返回 -1,格式不正确抛出 NumberFormatException。

  • long getDateHeader():(String name),读取日期型请求头。将包含 HTTP 日期格式(如 RFC 1123)的请求头自动转换为自 Epoch 起算的毫秒长整型时间戳。

  • String getContentType():(),获取请求体 MIME 媒体类型。返回 Header 中 Content-Type 的完整取值(如 application/json;charset=UTF-8)。

  • int getContentLength():(),获取请求体字节长度。返回 Header 中 Content-Length 的整型取值。若长度未知则返回 -1。

注意事项:

  1. 大小写不敏感规范:按照 RFC 7230 规范,HTTP Header 的名称是不区分大小写的。因此调用 getHeader("Accept") 与 getHeader("accept") 获取的结果完全相同。
  2. 超大请求体长度截断:getContentLength() 返回值最大为 Integer.MAX_VALUE(约 2GB)。若处理大文件上传或超大流,应改用 Servlet 3.1 引入的 getContentLengthLong() 方法获取 long 型长度,防止发生数值整型溢出。
java
package com.example.servlet.request.api;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RequestHeadersApiDemo extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    resp.setContentType("text/plain;charset=UTF-8");
    PrintWriter out = resp.getWriter();

    // 1. 读取单值与类型特化请求头
    String userAgent = req.getHeader("User-Agent");
    int contentLength = req.getContentLength();
    long ifModifiedSince = req.getDateHeader("If-Modified-Since");

    out.println("User-Agent: " + userAgent);
    out.println("Content-Length: " + contentLength);
    out.println("If-Modified-Since 时间戳: " + ifModifiedSince);

    // 2. 遍历所有请求头
    out.println("\n--- 全量请求头遍历 ---");
    Enumeration<String> headerNames = req.getHeaderNames();
    while (headerNames.hasMoreElements()) {
      String name = headerNames.nextElement();
      // 读取同名多值请求头
      Enumeration<String> values = req.getHeaders(name);
      StringBuilder valBuilder = new StringBuilder();
      while (values.hasMoreElements()) {
        valBuilder.append(values.nextElement()).append("; ");
      }
      out.println(name + " -> " + valBuilder.toString());
    }
  }
}

请求参数 ​

  • String getParameter():(String name),获取单值参数。根据参数名获取单个字符串参数值。若存在多个同名参数,仅返回容器解析出的第一个值。

  • String[] getParameterValues():(String name),获取多值参数数组。根据参数名获取同名参数的所有取值组成的数组(常见于多选 Checkbox 提交)。

  • Map<String, String[]> getParameterMap():(),获取不可变参数映射表。返回包含当前请求所有参数键值对的 Map 集合。

  • Enumeration<String> getParameterNames():(),获取所有参数名称。返回当前请求中所有参数键名的枚举集合。

  • void setCharacterEncoding():(String env),设置请求体解码字符集。显式覆盖容器解析 Body 参数时所使用的字符编码集(如 UTF-8)。

  • String getCharacterEncoding():(),获取当前字符编码。返回在请求头 Content-Type 中指定的字符集,若未明确声明且未设置过则返回 null。

注意事项:

  1. getParameterMap() 的不可变性:容器返回的 Map<String, String[]> 是受保护的只读视图(如 Tomcat 的 ParameterMap.setLocked(true))。直接对其执行 put 或 remove 会抛出 IllegalStateException 或 UnsupportedOperationException。
  2. GET 与 POST 参数的聚合性:getParameter 系列方法是聚合性的,它会同时合并 URL QueryString 与 POST Form-urlencoded Body 中的参数。若两者包含同名键,QueryString 往往优先排列在 String[] 数组的前列。
java
package com.example.servlet.request.api;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RequestParamsApiDemo extends HttpServlet {

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    // 1. 设置字符编码
    req.setCharacterEncoding("UTF-8");
    String encoding = req.getCharacterEncoding();

    resp.setContentType("text/plain;charset=UTF-8");
    PrintWriter out = resp.getWriter();
    out.println("当前解析使用的字符编码: " + encoding);

    // 2. 读取单值与多值参数
    String singleParam = req.getParameter("username");
    String[] multiParams = req.getParameterValues("hobbies");
    out.println("单值参数 username: " + singleParam);
    out.println("多值参数 hobbies: " + Arrays.toString(multiParams));

    // 3. 遍历参数映射表 Map
    out.println("\n--- 参数 Map 结构 ---");
    Map<String, String[]> paramMap = req.getParameterMap();
    for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
      out.println(entry.getKey() + " -> " + Arrays.toString(entry.getValue()));
    }

    // 4. 遍历所有参数键名
    out.println("\n--- 参数名枚举遍历 ---");
    Enumeration<String> names = req.getParameterNames();
    while (names.hasMoreElements()) {
      out.println("检测到参数名: " + names.nextElement());
    }
  }
}

请求体 ​

  • ServletInputStream getInputStream():(),获取二进制输入流。返回用于读取请求体原始二进制字节数据的输入流通道。

  • BufferedReader getReader():(),获取字符输入缓冲读取器。返回经过字符集解码包装的缓冲字符流,常用于读取 JSON、XML 格式的请求体。

注意事项:

  1. 互斥抛出规则:对于同一个请求对象,调用了 getInputStream() 之后,再调用 getReader() 会直接抛出 IllegalStateException;反之亦然。
  2. 异步非阻塞 IO 升级:在 Servlet 3.1+ 规范中,ServletInputStream 新增了 setReadListener(ReadListener listener) 方法,支持结合 @WebServlet(asyncSupported = true) 实现基于事件驱动的非阻塞流读取,适用于高并发网关代理。
java
package com.example.servlet.request.api;

import java.io.BufferedReader;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RequestBodyStreamApiDemo extends HttpServlet {

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    String contentType = req.getContentType();

    resp.setContentType("text/plain;charset=UTF-8");

    // 根据 Content-Type 策略选择字符流或字节流
    if (contentType != null && contentType.contains("application/json")) {
      // 1. 使用 getReader 读取结构化字符文本
      StringBuilder jsonPayload = new StringBuilder();
      try (BufferedReader reader = req.getReader()) {
        String line;
        while ((line = reader.readLine()) != null) {
          jsonPayload.append(line);
        }
      }
      resp.getWriter().write("成功以字符流消费 JSON: " + jsonPayload.toString());
    } else {
      // 2. 使用 getInputStream 读取二进制原生数据
      ServletInputStream inputStream = req.getInputStream();
      byte[] binaryData = inputStream.readAllBytes();
      resp.getWriter().write("成功以二进制字节流消费数据,长度: " + binaryData.length + " bytes");
    }
  }
}

请求域与流程分发 ​

  • Object getAttribute():(String name),读取请求作用域属性。获取通过程序存入当前 Request 对象的对象引用。

  • void setAttribute():(String name, Object o),存入请求作用域属性。绑定对象至 Request 域。若传入 null,等效于调用 removeAttribute。

  • void removeAttribute():(String name),移除请求作用域属性。从 Request 域中清除指定的属性键。

  • Enumeration<String> getAttributeNames():(),获取所有属性名称。返回当前 Request 对象绑定的所有属性键名枚举。

  • RequestDispatcher getRequestDispatcher():(String path),获取请求分发调度器。用于将当前请求在服务端内部转发(Forward)至其他资源或包含(Include)外部页面。

注意事项:

  1. 生命周期的瞬时性:Request 属性的生命周期仅存在于单次客户端请求中。一旦响应向客户端提交结束,所有绑定的属性即告废弃。
  2. 服务端转发的透明性:通过 req.getRequestDispatcher(path).forward(req, resp) 进行内部转发时,客户端浏览器的 URL 地址栏不会发生改变,且原先存入 Request 作用域的所有属性在目标 Servlet 中依然完全可用。
java
package com.example.servlet.request.api;

import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RequestScopeAndDispatchApiDemo extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {

    // 1. 设置业务处理中间属性
    req.setAttribute("OPERATOR_ID", "SYS_ADMIN_01");
    req.setAttribute("TRACE_TOKEN", "UUID-10023-9981");

    // 2. 检查与移除属性
    req.removeAttribute("TRACE_TOKEN");

    // 3. 读取属性并验证
    Object operatorId = req.getAttribute("OPERATOR_ID");

    // 4. 遍历所有属性名
    Enumeration<String> attributeNames = req.getAttributeNames();
    while (attributeNames.hasMoreElements()) {
      String attrName = attributeNames.nextElement();
      System.out.println("存在属性: " + attrName);
    }

    // 5. 将处理请求内部转发至内部渲染路径
    RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/internal-view.jsp");
    if (dispatcher != null && req.getParameter("forward") != null) {
      dispatcher.forward(req, resp);
    } else {
      resp.getWriter().write("操作人: " + operatorId);
    }
  }
}

会话 ​

  • Cookie[] getCookies():(),获取请求携带的 Cookie 数组。从 HTTP 请求报文的 Cookie 标头中解析并返回客户端提交的所有 Cookie 实例数组。若客户端本次请求未携带任何 Cookie,则返回 null。

  • HttpSession getSession():(),获取当前会话对象(无则创建)。等效于 getSession(true)。返回与当前请求关联的 HttpSession 对象;若当前请求尚未关联有效会话,则自动在服务端创建并分配一个新会话。

  • HttpSession getSession():(boolean create),按需获取或创建会话对象。若当前请求存在有效会话则直接返回;若不存在有效会话:当 create 为 true 时新建并返回会话,当 create 为 false 时直接返回 null(不会主动创建新会话)。

注意事项:

  1. getCookies() 空指针陷阱(NPE):当客户端首次访问、禁用 Cookie 或本次请求未包含任何 Cookie 时,req.getCookies() 会直接返回 null 而非空数组。在遍历或对数组执行任何操作前,必须显式进行非空判定(cookies != null),否则极易引发 NullPointerException。
  2. getSession() 与 getSession(false) 的选用策略:
    • getSession()(等同于 getSession(true))具有“副作用”:若会话不存在则强制创建新会话,并在响应中自动下发 Set-Cookie: JSESSIONID=...。
    • 在身份验证校验、拦截器/过滤器鉴权、或无状态只读操作中,若只是为了判断用户是否已登录,必须优先使用 req.getSession(false)。若盲目调用 getSession(),网络爬虫或高频未登录请求将导致服务器无节制地创建大量空 Session,迅速耗尽堆内存甚至引发 OOM。
  3. Cookie 字符集与中文编解码:RFC 6265 规范限制 Cookie 值中不能包含分号、逗号、空格以及任意非 ASCII 字符。若需在 Cookie 中存储中文或复杂字符串,在通过 response.addCookie() 写入前必须使用 URLEncoder.encode(val, "UTF-8") 编码,并在通过 req.getCookies() 读取后使用 URLDecoder.decode(val, "UTF-8") 进行解码。
java
package com.example.servlet.request.api;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class RequestSessionAndCookieApiDemo extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    resp.setContentType("text/plain;charset=UTF-8");
    PrintWriter out = resp.getWriter();

    // 1. 读取并遍历客户端携带的 Cookie(防范 null 引发 NPE)
    Cookie[] cookies = req.getCookies();
    if (cookies != null) {
      out.println("--- 客户端提交的 Cookie 列表 ---");
      for (Cookie cookie : cookies) {
        String name = cookie.getName();
        // 对可能包含特殊字符或中文的内容进行 URL 解码
        String value = URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8);
        out.println(String.format("Cookie 键: %s, 解码值: %s", name, value));
      }
    } else {
      out.println("客户端本次请求未携带任何 Cookie");
    }

    // 2. 按需读取已存在会话(只读操作,避免无意义创建 Session 消耗服务端内存)
    HttpSession existingSession = req.getSession(false);
    if (existingSession == null) {
      out.println("\n当前无活跃会话(getSession(false) 返回 null)");
    } else {
      out.println("\n检测到已存在的活跃会话,Session ID: " + existingSession.getId());
    }

    // 3. 获取或强制新建会话,并存取会话域属性
    HttpSession session = req.getSession(); // 等效于 req.getSession(true)
    session.setAttribute("USER_AUTH", "AUTH_TOKEN_SAMPLE");
  }
}

网络信息 ​

  • String getRemoteAddr():(),获取客户端 IP 地址。返回直接与当前服务器建立 TCP Socket 连接的客户端/反向代理 IP 字符串。

  • int getRemotePort():(),获取客户端源端口号。返回客户端建立 TCP 连接时所使用的临时端口号。

  • String getLocalAddr():(),获取服务端接收 IP 地址。返回服务器本机承接该网络连接的网络接口 IP 地址。

  • int getServerPort():(),获取服务端服务端口。返回承接请求的目标虚拟主机端口号。

  • boolean isSecure():(),是否为安全加密连接。指示当前请求是否通过 SSL/TLS(HTTPS)安全信道传输。

注意事项:

  1. 反向代理下的真实 IP 穿透:当部署架构中存在 Nginx、HAProxy、SLB 等反向代理或 CDN 时,getRemoteAddr() 获取到的将是最后一跳代理服务器的内网 IP,而非客户端真实 IP。获取真实 IP 必须优先依次提取 X-Forwarded-For、X-Real-IP 请求头并进行合法性校验。
  2. 伪造 Header 欺骗防御:客户端可自行在请求头伪造 X-Forwarded-For。在安全性要求极高的场景下(如支付鉴权),必须校验反向代理链路的白名单,或在 Nginx 接入层统一强制重写覆盖该 Header。
java
package com.example.servlet.request.api;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class NetworkMetadataApiDemo extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    // 1. 网络底层 Socket 元数据
    String socketClientIp = req.getRemoteAddr();
    int socketClientPort = req.getRemotePort();
    String serverLocalIp = req.getLocalAddr();
    int serverPort = req.getServerPort();
    boolean secureChannel = req.isSecure();

    // 2. 穿透反向代理识别真实客户端 IP
    String realClientIp = req.getHeader("X-Forwarded-For");
    if (realClientIp == null || realClientIp.isEmpty() || "unknown".equalsIgnoreCase(realClientIp)) {
      realClientIp = req.getHeader("X-Real-IP");
    }
    if (realClientIp == null || realClientIp.isEmpty() || "unknown".equalsIgnoreCase(realClientIp)) {
      realClientIp = socketClientIp;
    } else if (realClientIp.contains(",")) {
      // 多级代理场景下,第一个 IP 为真实客户端 IP
      realClientIp = realClientIp.split(",")[0].trim();
    }

    resp.setContentType("text/plain;charset=UTF-8");
    resp.getWriter().write(String.format(
      "直连 Socket 客户端: %s:%d\n" +
      "识别真实客户端 IP: %s\n" +
      "服务端本地绑定: %s:%d\n" +
      "通道是否加密 (HTTPS): %b\n",
      socketClientIp, socketClientPort, realClientIp, serverLocalIp, serverPort, secureChannel
    ));
  }
}

实战:用户注册 ​

image-20260909160612063

需求分析与控件映射

本实战案例模拟典型的用户注册场景,综合运用 HttpServletRequest 核心参数获取 API。表单控件与后端解析方法的映射关系如下:

表单控件HTML 元素类型对应参数名 (name)后端获取方法核心说明
用户名称<input type="text">usernamereq.getParameter("username")单值文本,需预先配置请求体解码字符集
用户密码<input type="password">passwordreq.getParameter("password")单值密文输入
确认密码<input type="password">confirmPasswordreq.getParameter("confirmPassword")服务端核验两次密码是否一致
运动项目<input type="checkbox">sportsreq.getParameterValues("sports")多值参数,全未勾选时返回 null
选择性别<input type="radio">genderreq.getParameter("gender")单选框,获取被选中的 value 取值
选择城市<select> 下拉菜单cityreq.getParameter("city")单选下拉框,读取所选 <option> 的 value
自我介绍<textarea> 多行文本introreq.getParameter("intro")纯文本多行字符串,保留内部空白与换行
头像文件<input type="file">avatarreq.getPart("avatar")二进制多部件文件段,需搭配 @MultipartConfig

前端表单设计(register.html)

前端表单采用 POST 方式提交至后端 Servlet,表单中包含文件选择框,需配置 enctype="multipart/form-data":

html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>用户注册</title>
  </head>
  <body>
    <h3>用户注册信息</h3>
    <form action="register" method="post" enctype="multipart/form-data">
      用户名称: <input type="text" name="username" /><br />
      用户密码: <input type="password" name="password" /><br />
      确认密码: <input type="password" name="confirmPassword" /><br />

      选择你喜欢的运动项目:
      <input type="checkbox" name="sports" value="篮球" />篮球<br />
      <input type="checkbox" name="sports" value="足球" checked />足球<br />
      <input type="checkbox" name="sports" value="手球" checked />手球<br />

      请选择性别:
      <input type="radio" name="gender" value="男" />男<br />
      <input type="radio" name="gender" value="女" />女<br />

      请选择城市:
      <select name="city">
        <option value="">--选择--</option>
        <option value="北京">北京</option>
        <option value="上海">上海</option>
        <option value="广州">广州</option>
        <option value="深圳">深圳</option></select
      ><br />

      自我介绍:
      <textarea name="intro" rows="5" cols="25"></textarea><br />

      选择你的文件(头像) <input type="file" name="avatar" /><br />

      <input type="submit" value="提交" />
      <input type="reset" value="重置" />
    </form>
  </body>
</html>

后端 Servlet 实现(RegisterServlet.java)

后端通过继承 HttpServlet 并重写 doPost 方法,演示单值、多值参数以及二进制文件段的提取与回显:

java
package com.hspedu.servlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

@WebServlet("/register")
@MultipartConfig // 启用 Servlet 3.0+ 原生文件上传解析支持
public class RegisterServlet extends HttpServlet {

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    // 1. 设置请求体解码字符集(必须在首次读取参数前调用,彻底解决 POST 中文乱码)
    req.setCharacterEncoding("UTF-8");

    // 2. 设置响应头与输出流编码格式
    resp.setContentType("text/html;charset=UTF-8");
    PrintWriter out = resp.getWriter();

    // 3. 读取单值普通文本参数
    String username = req.getParameter("username");
    String password = req.getParameter("password");
    String confirmPassword = req.getParameter("confirmPassword");
    String gender = req.getParameter("gender");
    String city = req.getParameter("city");
    String intro = req.getParameter("intro");

    // 4. 读取多值复选框参数(Checkbox 提交的数组)
    String[] sports = req.getParameterValues("sports");

    // 5. 提取二进制头像文件项
    Part avatarPart = req.getPart("avatar");
    String avatarFileName = (avatarPart != null && avatarPart.getSize() > 0)
        ? avatarPart.getSubmittedFileName()
        : "未选择上传文件";
    long avatarSize = (avatarPart != null) ? avatarPart.getSize() : 0;

    // 6. 基础业务校验(核验密码一致性)
    if (password == null || !password.equals(confirmPassword)) {
      out.println("<h3 style='color:red;'>注册失败:两次输入的密码不一致!</h3>");
      out.println("<p><a href='javascript:history.back()'>返回重新填写</a></p>");
      return;
    }

    // 7. 回显展示接收并解析完毕的注册信息
    out.println("<h2>用户注册成功!提交信息如下:</h2>");
    out.println("<ul>");
    out.println("  <li><strong>用户名称:</strong>" + username + "</li>");
    out.println("  <li><strong>用户密码:</strong>" + password + "</li>");
    out.println("  <li><strong>运动项目:</strong>" + (sports != null ? Arrays.toString(sports) : "未勾选任何项目") + "</li>");
    out.println("  <li><strong>所选性别:</strong>" + (gender != null ? gender : "未选择") + "</li>");
    out.println("  <li><strong>所属城市:</strong>" + (city != null && !city.isEmpty() ? city : "未选择") + "</li>");
    out.println("  <li><strong>自我介绍:</strong><pre>" + intro + "</pre></li>");
    out.println("  <li><strong>头像附件:</strong>" + avatarFileName + " (" + avatarSize + " 字节)</li>");
    out.println("</ul>");
  }
}

核心细节与避坑指南

  1. Checkbox 多值参数的空指针防范:若用户未勾选任何运动选项直接点击提交,浏览器在请求体中不会携带 sports 键,此时 req.getParameterValues("sports") 将返回 null(而非空数组 new String[0])。直接对 sports 进行遍历或访问 sports.length 会抛出 NullPointerException,业务代码中务必执行 sports != null 的防御性校验。
  2. POST 参数解码的时效性:req.setCharacterEncoding("UTF-8") 的生效前提是在首次调用任何 getParameter() 或 getReader() 之前执行。若在此之前读取过任何参数,底层流的解析解码器将被初始化并锁定,后续再次调用 setCharacterEncoding() 将完全失效,导致中文仍呈乱码状态。
  3. enctype="multipart/form-data" 协同要求:当表单包含文件项时必须声明该编码类型;而在服务端,Servlet 必须显式标注 @MultipartConfig 注解。若遗漏该注解,Tomcat 不会解析多段数据,此时不仅 req.getPart() 会抛出异常,所有通过 req.getParameter() 提取的普通文本项也会全部变为 null。

HttpServletResponse ​

javax.servlet.http.HttpServletResponse 是 Java Web 开发中用于封装 HTTP 响应信息的顶层接口。

  • 职责边界:当客户端向服务器发起请求时,Servlet 容器(如 Apache Tomcat)会同时创建一对对象——HttpServletRequest 与 HttpServletResponse。HttpServletResponse 专门负责向客户端浏览器发送 HTTP 状态码、响应头(Headers)以及实体内容(Body)。
  • 协议绑定:该接口继承自通用的 javax.servlet.ServletResponse,并专门扩展了针对 HTTP/HTTPS 协议相关的方法(如重定向、Cookie 写入、状态码操控等)。

报文映射 ​

HTTP 协议基于纯文本传输,HttpServletResponse 的所有核心方法均与 HTTP 响应报文结构保持一一对应。

  • 状态行:包含协议版本、状态码与状态描述(如 HTTP/1.1 200 OK),对应 setStatus() 与 sendError()。
  • 响应头:键值对集合(如 Content-Type: text/html),对应 setHeader()、addHeader() 及各类快捷配置方法。
  • 空行:回车换行符(CRLF),用于分隔首部与实体。
  • 响应体(附属体):最终返回给浏览器渲染展示的数据,通过 getWriter() 或 getOutputStream() 输出流逐字节/字符写入。

image-20260909172021963

状态码控制 ​

状态码由 3 位数字组成,用于向客户端指示该次请求的处理状态与结果类型:

java
// 设置自定义成功或操作状态码
resp.setStatus(200);

// 校验未通过时向客户端发送禁止访问状态码
resp.sendError(403, "Access Denied");
  • void setStatus():(int sc),设置 HTTP 响应状态码。向响应状态行注入指定的标准 HTTP 状态码(如 200、201、404 等)。

  • void sendError():(int sc),派发标准错误状态码。向客户端发送指定的 HTTP 错误状态码,并使用容器自带的默认 HTML 错误页面模板渲染响应正文。

  • void sendError():(int sc, String msg),派发自定义错误状态码。向客户端发送指定的状态码,并将自定义的描述消息附加在默认错误模板中。

响应头设置 ​

响应头用于指示客户端浏览器该如何解析接收到的数据、是否缓存以及是否执行页面跳转:

java
// 配置禁止浏览器缓存当前动态内容
resp.setHeader("Cache-Control", "no-cache");

// 设置响应内容格式与字符集编码
resp.setContentType("text/html;charset=UTF-8");
  • void setHeader():(String name, String value),覆盖指定响应头。设置指定名称的 HTTP 响应头。如果该响应头已存在,其先前的值将被完全覆盖。

  • void addHeader():(String name, String value),追加指定响应头。向指定名称的 HTTP 响应头追加新值,常用于允许多个同名字段的响应头(如 Set-Cookie)。

  • void setDateHeader():(String name, long date),覆盖时间戳响应头。将自 1970-01-01 算起的毫秒级时间戳转换为标准 HTTP 日期格式(RFC 1123)并写入响应头。

  • 常用响应头:

    • 禁用缓存:同时设置 Cache-Control: no-cache、Pragma: no-cache、Expires: -1。
    • 定时跳转:设置 Refresh: 3;url=/login.html,指示浏览器 3 秒后自动跳转至指定页面。

输出流机制 ​

向客户端输出响应正文支持字符流与字节流两种途径:

java
// 获取字符输出流并写入响应正文
PrintWriter out = resp.getWriter();
out.println("Hello World");
// 显式刷新缓冲区将内容推送给客户端
out.flush();
  • ServletOutputStream getOutputStream():(),获取二进制输出流。返回适用于写出原生字节数据的 ServletOutputStream 对象,主要用于文件下载、多媒体流传输或图片渲染等场景。

  • PrintWriter getWriter():(),获取文本字符输出流。返回基于指定字符编码的 PrintWriter 字符打印流,适用于输出 HTML、JSON、XML 等纯文本数据。

  • 互斥约束:在同一次请求响应生命周期中,getWriter() 与 getOutputStream() 严禁同时调用,否则容器将抛出 IllegalStateException。

  • 缓冲刷新:容器默认具备内置缓冲区(通常为 8KB)。当显式调用 flush()、缓冲区满或 Servlet 执行完毕时,数据正式提交并写入网络 Socket 通道。一旦响应被提交(isCommitted() == true),将无法再更改状态码和响应头。

编码处理 ​

若服务端输出中文字符时未指定统一编码,容易因默认采用 ISO-8859-1 产生乱码:

java
// 必须在获取输出流之前声明内容类型与字符集
resp.setContentType("text/html;charset=UTF-8");
// 获取流后向客户端输出中文字符
resp.getWriter().println("你好,世界!");
  • 核心原理:resp.setContentType("text/html;charset=UTF-8") 同时完成了两项任务:指定 Tomcat 内部采用 UTF-8 编码将字符转换为字节流;在 HTTP 响应头中注入 Content-Type: text/html;charset=UTF-8,指示浏览器以 UTF-8 规则进行解码渲染。
  • 顺序约束:字符集配置代码必须放在调用 resp.getWriter() 之前执行,否则输出流一旦创建,默认字符集即被绑定,后续配置无效。

请求重定向 ​

请求重定向(Client Redirect) 是一种由客户端浏览器驱动的页面跳转机制。

当客户端向服务器发送请求时,当前 Servlet 并不直接返回最终目标内容,而是向浏览器返回一个重定向状态码(通常为 302 Found)以及一个包含目标地址的响应头(Location)。浏览器接收到该响应后,会自动以 GET 方式向该新地址重新发起一次独立的 HTTP 请求,最终获取目标资源并渲染呈现。

运作机制 ​

请求重定向最本质的特征是两次独立的 HTTP 网络往返,跳转过程完全暴露在客户端层面。

如上图所示,重定向由客户端与服务器交互完成:

  • 初次请求:客户端向服务器发起 URL1 资源请求。
  • 告知重定向:服务器返回包含目标 URL2 的响应信息(包含 302 状态码与 Location 响应头)。
  • 二次请求:客户端读取新地址,主动向 URL2 发起第二次 HTTP 请求。
  • 最终响应:服务器定位到 URL2 资源并回写最终内容。

image-20260909180708224

image-20260909174254610

实现方式 ​

在 Servlet 中,重定向主要通过 HttpServletResponse 提供的 API 来完成:

java
// 方式一:调用封装好的快捷 API 完成重定向
resp.sendRedirect(req.getContextPath() + "/success.html");

// 方式二:底层等价实现,手动设置状态码与响应头
resp.setStatus(302);
resp.setHeader("Location", req.getContextPath() + "/success.html");
  • void sendRedirect():(String location),发送重定向指令。向客户端发送临时重定向响应(HTTP 302 状态码),浏览器接收到后将自动向新的 location 地址发起全新请求。
  • 底层原理:两者在 HTTP 网络协议层面的效果完全相同。

路径规范 ​

因为重定向是让浏览器重新发起连接,所以路径的基准解析由浏览器完成,而非服务器内部:

  • 站内绝对路径:重定向到本项目的其他页面时,路径必须显式拼接应用上下文(Context Path),例如 req.getContextPath() + "/index.jsp"。如果误写成 /index.jsp,浏览器会默认从服务器根域名(http://localhost:8080/index.jsp)发起请求,导致 404 Not Found 错误。
  • 站外绝对路径:重定向完全支持跳转至外部站点,直接传入完整的 URL 即可,例如 resp.sendRedirect("[https://www.example.com](https://www.example.com)")。
  • 受保护资源限制:重定向无法直接访问服务器的 /WEB-INF/ 目录,因为该目录受 Web 容器安全保护,禁止由外网或浏览器直接寻址。

典型应用 ​

请求重定向在 Web 开发中最常用于表单重复提交防护(PRG 模式:Post-Redirect-Get)以及身份认证鉴权:

  • PRG 模式:用户提交订单(POST 请求)后,若直接转发到成功页,用户刷新浏览器会导致表单二次提交。采用重定向到成功页(GET 请求),刷新操作仅会重复加载静态展示页。
  • 登录鉴权:未登录用户访问受限资源时,过滤器或 Servlet 将请求重定向至登录页 login.html。

实战:文件下载重定向 ​

image-20260909180446965

需求分析与流程设计

本实战案例模拟基于 sendRedirect 实现文件下载入口引导与静态资源重定向分发:

  1. 页面重定向(入口引导):客户端访问下载入口 Servlet(如 /down)时,服务端经业务处理后,调用 resp.sendRedirect(req.getContextPath() + "/down.html") 发送 302 临时重定向,引导浏览器跳转至下载引导页面。此时浏览器地址栏显式变更为 http://localhost:8080/servlet/down.html(如上图所示)。
  2. 资源重定向(触发下载):在 down.html 页面中点击“下载天龙八部”超链接,请求发送给后端的 DownServlet。Servlet 完成校验或日志记录后,再次调用 resp.sendRedirect(req.getContextPath() + "/download/天龙八部.zip") 将请求重定向至服务器静态资源目录下的目标压缩包文件,由浏览器直接向该地址发起二次 GET 请求,触发文件下载保存。

前端页面设计(down.html)

在 Web 应用根目录下创建 down.html,展示下载标题及下载超链接:

html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>下载文件</title>
  </head>
  <body>
    <h1>下载文件</h1>
    <!-- 点击超链接请求后端 Servlet 进行重定向分发 -->
    <a href="downServlet">下载天龙八部</a>
  </body>
</html>

提示:此处 <a href="downServlet"> 采用相对路径,当页面位于 http://localhost:8080/servlet/down.html 时,浏览器会自动解析为 http://localhost:8080/servlet/downServlet;亦可写作包含 Context Path 的站内绝对路径 href="/servlet/downServlet"。

后端 Servlet 实现(DownServlet.java)

后端通过继承 HttpServlet,在 doGet 中利用 resp.sendRedirect 完成页面跳转与资源下载的重定向分发:

java
package com.hspedu.servlet;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns = {"/down", "/downServlet"})
public class DownServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    String servletPath = req.getServletPath();

    // 场景一:访问 /down 入口,重定向至前端静态下载展示页 down.html
    if ("/down".equals(servletPath)) {
      resp.sendRedirect(req.getContextPath() + "/down.html");
      return;
    }

    // 场景二:在页面点击“下载天龙八部”链接(请求 /downServlet),重定向至实际资源下载路径
    if ("/downServlet".equals(servletPath)) {
      // 目标资源文件名(存放在 webapp/download/ 目录下)
      String fileName = "天龙八部.zip";

      // 对含中文的文件名进行 URL 编码,避免 HTTP Location 响应头非法字符报错
      String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());

      // 发送 302 重定向,指引浏览器直接向实际文件路径发起下载请求
      resp.sendRedirect(req.getContextPath() + "/download/" + encodedFileName);
    }
  }

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    doGet(req, resp);
  }
}
  • 显式拼接上下文路径:重定向是由客户端浏览器重新发起 HTTP 请求,因此站内重定向路径必须以 / 开头并显式携带 req.getContextPath()(即 /servlet),否则浏览器会默认从服务器根域名寻址导致 404 错误。
  • 中文 URL 转码:HTTP Location 响应头仅支持 ASCII 字符。若重定向路径中包含中文字符(如“天龙八部”),必须使用 URLEncoder.encode() 进行百分号编码转义,防止触发非法响应头字符异常。
  • 业务解耦与前置控制:通过 Servlet 进行下载重定向,可以在实际下载前执行权限核验、下载计次、动态分流(如重定向至第三方 CDN)等逻辑,比在前端直接暴露静态文件链接更为安全灵活。

文件下载@ ​

当需要服务端提供文件下载而非在浏览器内直接预览打开时,需结合响应头与二进制字节流实现:

java
// 设置响应头通知浏览器以附件形式保存并指定默认文件名
resp.setHeader("Content-Disposition", "attachment;filename=\"report.pdf\"");

// 设置响应格式为通用二进制数据流
resp.setContentType("application/octet-stream");

// 获取字节输出流写回二进制文件数据
ServletOutputStream out = resp.getOutputStream();
  • Content-Disposition:声明为 attachment 会强制浏览器触发“文件下载保存”对话框,filename 指定默认存储文件名。
  • Content-Type:声明为 application/octet-stream 表示通用的二进制数据流,避免浏览器自动执行内联渲染(Inline View)。

执行流程 ​

从客户端发起请求至响应完成经历以下标准步骤:

  1. 实例创建:Tomcat 容器接收 HTTP 连接,完成请求解析后,同步创建 HttpServletRequest 与 HttpServletResponse 实例。

  2. 逻辑调用:容器指派工作线程调用目标 Servlet 的 service() 方法,并将 request 和 response 传递给开发者的 doGet() 或 doPost()。

  3. 数据组装:业务代码设置状态码、定义响应头,并通过字符流或字节流向底层输出缓冲区填充响应体数据。

  4. 提交响应:当输出缓冲区填满、开发者调用 flush() 或 Servlet 方法执行结束时,容器触发响应提交(Commit),组装出标准 HTTP 格式报文。

  5. 通道回写与销毁:底层网络组件将组装好的报文通过 TCP 通道写回客户端,随后容器将 response 对象销毁或归还对象池以备复用。

API: HttpServletResponse ​

状态码 ​

  • void setStatus():(int sc),设置 HTTP 响应状态码。向响应状态行注入指定的标准 HTTP 状态码(如 200、201、404 等)。

  • int getStatus():(),获取当前响应状态码。返回当前响应对象中已挂载的 HTTP 状态码。

注意事项:

  1. 原规范中的 setStatus(int sc, String sm) 方法因允许传入非标准状态描述文本(Reason Phrase),在 Servlet 2.1 之后已被废弃(Deprecated),严禁在新项目中使用。
  2. setStatus() 必须在响应被正式提交(Committed)之前调用。若响应已提交,调用此方法将不会产生任何网络写出效果,且可能在容器日志中引发警告。
  3. 建议直接引用 HttpServletResponse 中预定义的整型常量(例如 HttpServletResponse.SC_OK、HttpServletResponse.SC_NOT_FOUND),避免硬编码数字。
java
import jakarta.servlet.http.HttpServletResponse;

public class StatusCodeManager {

  public static void configureStatus(HttpServletResponse response, boolean resourceCreated) {
    if (resourceCreated) {
      // 201 Created: 表示资源已被成功创建
      response.setStatus(HttpServletResponse.SC_CREATED);
    } else {
      // 200 OK: 标准请求处理成功
      response.setStatus(HttpServletResponse.SC_OK);
    }

    // 读取当前设置的状态码并记录
    int currentStatus = response.getStatus();
    System.out.println("当前响应状态码: " + currentStatus);
  }
}

响应头 ​

  • void setHeader():(String name, String value),覆盖指定响应头。设置指定名称的 HTTP 响应头。如果该响应头已存在,其先前的值将被完全覆盖。

  • void addHeader():(String name, String value),追加指定响应头。向指定名称的 HTTP 响应头追加新值,常用于允许多个同名字段的响应头(如 Set-Cookie)。

  • void setIntHeader():(String name, int value),覆盖整型响应头。将指定响应头的值设定为整型数字的字符串形式。若已存在则直接覆盖。

  • void addIntHeader():(String name, int value),追加整型响应头。向指定响应头追加一个整型数值。

  • void setDateHeader():(String name, long date),覆盖时间戳响应头。将自 1970-01-01 算起的毫秒级时间戳转换为标准 HTTP 日期格式(RFC 1123)并写入响应头。

  • void addDateHeader():(String name, long date),追加时间戳响应头。将毫秒级时间戳转换为标准 HTTP 日期字符串并追加至指定头字段。

  • boolean containsHeader():(String name),检查响应头是否存在。判断当前响应是否已经设置了指定名称的响应头(字段名大小写不敏感)。

  • String getHeader():(String name),获取首个响应头值。根据名称返回当前响应头中记录的第一个值。

  • Collection<String> getHeaders():(String name),获取同名响应头集合。返回与指定头名称关联的所有值的不可变集合。

  • Collection<String> getHeaderNames():(),获取全部响应头名称。返回当前响应对象中包含的所有响应头名称的集合。

注意事项:

  1. HTTP 响应头字段名称不区分大小写,但规范建议采用首字母大写的连字符标准写法(如 Content-Disposition、Cache-Control)。
  2. setHeader() 与 addHeader() 的核心差异在于覆盖还是追加。对于只允许单个值的字段(如 Content-Type、Location)必须使用 setHeader();而对于允许逗号分隔或多行的头(如 Cache-Control)可使用 addHeader()。
java
import jakarta.servlet.http.HttpServletResponse;
import java.util.Collection;

public class ResponseHeaderManager {

  public static void setupSecurityAndCacheHeaders(HttpServletResponse response) {
    // 1. 覆盖方式写入安全防护头
    response.setHeader("X-Content-Type-Options", "nosniff");
    response.setHeader("X-Frame-Options", "DENY");

    // 2. 追加方式设置多值头
    response.addHeader("Cache-Control", "no-cache");
    response.addHeader("Cache-Control", "no-store");

    // 3. 数值与日期辅助方法
    response.setIntHeader("X-Rate-Limit-Remaining", 99);
    response.setDateHeader("Expires", System.currentTimeMillis() + 3600_000L);

    // 4. 检查与读取已注入的头
    if (response.containsHeader("X-Frame-Options")) {
      String frameOption = response.getHeader("X-Frame-Options");
      System.out.println("Frame 配置: " + frameOption);
    }

    Collection<String> cacheControls = response.getHeaders("Cache-Control");
    System.out.println("Cache-Control 规则数量: " + cacheControls.size());

    Collection<String> allHeaderNames = response.getHeaderNames();
    System.out.println("当前响应头总项数: " + allHeaderNames.size());
  }
}

内容元数据 ​

  • void setContentType():(String type),设置内容媒体类型。设定发送给客户端的 MIME 类型(如 application/json、text/html),同时可包含字符编码参数(如 text/html;charset=UTF-8)。

  • String getContentType():(),获取当前内容媒体类型。返回当前已配置的 MIME 类型字符串(若包含字符集则一并返回)。

  • void setCharacterEncoding():(String charset),显式设定字符集编码。指定通过 getWriter() 输出字符数据时使用的字符编码格式(如 UTF-8)。

  • String getCharacterEncoding():(),获取当前字符集编码。返回响应正文输出所采用的字符集名称。

  • void setContentLength():(int len),设置响应体长度。设置 HTTP 响应头中的 Content-Length 字段(适用于 2GB 以内的数据)。

  • void setContentLengthLong():(long len),设置长整型响应体长度。用于指定超过 2GB(231−12^{31}-1 字节)的超大响应体数据长度。

注意事项:

  1. setContentType() 与 setCharacterEncoding() 必须在调用 getWriter() 之前以及响应提交(Committed)之前生效。若在 getWriter() 已经实例化之后再更改编码,该修改会被容器直接忽略。
  2. setContentLength() 不可在使用分块传输编码(Chunked Transfer Encoding)时随意设定。如果容器发现没有手动显式设定 Content-Length 且响应体超过了缓冲区大小,通常会自动切换为 Transfer-Encoding: chunked 模式发送。
java
import jakarta.servlet.http.HttpServletResponse;

public class ContentMetadataConfigurator {

  public static void configureJsonMetadata(HttpServletResponse response, int payloadByteLength) {
    // 显式指定编码方式(推荐在 getWriter 之前执行)
    response.setCharacterEncoding("UTF-8");

    // 设置标准 JSON 媒体类型
    response.setContentType("application/json");

    // 显式指定正文长度(非必要,但能提升客户端接收体验)
    response.setContentLength(payloadByteLength);

    // 获取并核验最终生效的元数据
    String effectiveType = response.getContentType();
    String effectiveEncoding = response.getCharacterEncoding();
    System.out.printf("生效 Content-Type: %s | 字符集: %s\n", effectiveType, effectiveEncoding);
  }

  public static void configureLargeBinaryMetadata(HttpServletResponse response, long totalFileBytes) {
    response.setContentType("application/octet-stream");
    // 超过 2GB 的文件必须使用 setContentLengthLong
    response.setContentLengthLong(totalFileBytes);
  }
}

输出流与字符流 ​

  • ServletOutputStream getOutputStream():(),获取二进制输出流。返回适用于写出原生字节数据的 ServletOutputStream 对象,主要用于文件下载、多媒体流传输或图片渲染等场景。

  • PrintWriter getWriter():(),获取文本字符输出流。返回基于指定字符编码的 PrintWriter 字符打印流,适用于输出 HTML、JSON、XML 等纯文本数据。

注意事项:

  1. 不可兼得限制:同一响应中不可混用 getOutputStream() 和 getWriter()。若先调用了 getOutputStream(),后续代码再次调用 getWriter()(反之亦然),容器将直接抛出 IllegalStateException: getOutputStream() has already been called for this response。
  2. 关闭流规范:一般情况下,Servlet 开发者无需在 finally 块中手动调用 out.close()。Servlet 容器在请求服务方法执行结束后,会自动刷新并关闭输出流。若过早手动显式 close(),将导致后续 Filter 无法拦截或追加响应内容。
java
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

public class ResponseStreamHandler {

  // 方式 A:基于字符流输出文本(JSON/HTML)
  public static void writeTextResponse(HttpServletResponse response, String textPayload)
      throws IOException {
    response.setContentType("text/plain;charset=UTF-8");
    PrintWriter writer = response.getWriter();
    writer.print(textPayload);
    // 注意:无需手动 writer.close(),交给容器生命周期管理
  }

  // 方式 B:基于字节流输出二进制数据(文件下载/图片)
  public static void writeBinaryResponse(HttpServletResponse response, byte[] binaryData)
      throws IOException {
    response.setContentType("application/octet-stream");
    ServletOutputStream outputStream = response.getOutputStream();
    outputStream.write(binaryData);
    outputStream.flush();
  }
}

缓冲机制 ​

  • void setBufferSize():(int size),设定首选缓冲区大小。请求容器为响应正文分配指定大小的底层字节缓冲区。容器会分配一个至少为此大小的缓冲区。

  • int getBufferSize():(),获取实际缓冲区大小。返回当前响应底层实际分配的缓冲区大小(字节)。

  • void flushBuffer():(),强制刷入网络层。将缓冲区中当前缓存的所有内容立即强制写入底层网络套接字(Socket)。此调用将直接导致响应被标记为已提交(Committed)。

  • void resetBuffer():(),清空正文缓冲区。仅清除缓冲区中尚未写出的正文内容,但保留当前已设置的状态码和所有响应头。

  • void reset():(),重置整个响应。彻底清除缓冲区中的数据,同时重置所有的状态码和响应头为初始状态。

  • boolean isCommitted():(),检测响应是否已提交。返回布尔值,指示是否已有 HTTP 状态行和部分头信息已经实际发送到底层网络。

注意事项:

  1. setBufferSize() 必须在有任何正文数据写入之前调用,否则将抛出 IllegalStateException。
  2. 当 isCommitted() 返回 true 时,严禁调用 reset() 或 resetBuffer(),否则会立即触发 IllegalStateException: Cannot reset buffer - response has already been committed。
  3. 一旦缓冲区累积的数据量超过了预设的 bufferSize,容器会自动隐式触发 flushBuffer(),从而使响应进入已提交状态。因此在处理长文本或大数据流时需格外警惕状态提交的时机。
java
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class ResponseBufferController {

  public static void processWithRollbackCapability(HttpServletResponse response) throws IOException {
    // 1. 在写入前扩容缓冲区(例如分配 32KB)
    response.setBufferSize(32 * 1024);
    System.out.println("实际缓冲区大小: " + response.getBufferSize() + " 字节");

    response.setContentType("text/plain;charset=UTF-8");
    PrintWriter writer = response.getWriter();

    try {
      writer.print("正在处理复杂运算第一阶段...\n");

      // 模拟业务异常发生
      boolean businessErrorOccurred = true;
      if (businessErrorOccurred) {
        throw new RuntimeException("业务流水号计算失败");
      }

      // 成功时提交
      response.flushBuffer();
    } catch (Exception ex) {
      // 2. 检查是否已被提交,若未提交则回滚缓冲区输出
      if (!response.isCommitted()) {
        response.reset(); // 清空正文并重置状态码及响应头
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        response.setContentType("text/plain;charset=UTF-8");
        response.getWriter().write("系统异常回滚: " + ex.getMessage());
      } else {
        System.err.println("响应已提前提交,无法执行重置操作!");
      }
    }
  }
}

页面重定向与错误派发 ​

  • void sendRedirect():(String location),发送重定向指令。向客户端发送临时重定向响应(HTTP 302 状态码),浏览器接收到后将自动向新的 location 地址发起全新请求。

  • void sendError():(int sc),派发标准错误状态码。向客户端发送指定的 HTTP 错误状态码,并使用容器自带的默认 HTML 错误页面模板渲染响应正文。

  • void sendError():(int sc, String msg),派发自定义错误状态码。向客户端发送指定的状态码,并将自定义的描述消息附加在默认错误模板中。

注意事项:

  1. 重定向 vs 转发(Forward):
  • sendRedirect:属于客户端行为。浏览器地址栏会发生改变,产生两次独立的 HTTP 请求,无法通过 request.getAttribute() 共享上文数据。支持跨域跳转到外部完整 URL。
  • forward:属于服务端行为。浏览器地址栏不变,仅一次 HTTP 请求,内部共享同一个 HttpServletRequest 作用域。只能在当前 Web 应用内部进行转发。
  1. 代码不会隐式中断:调用 sendRedirect() 或 sendError() 之后,当前方法后续的代码依然会继续向下执行。为避免出现逻辑混乱或再次写入数据引发异常,必须在调用后显式加上 return;。
  2. 若响应已被提交(isCommitted() == true),再次调用 sendRedirect() 或 sendError() 将直接抛出 IllegalStateException。
java
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public class NavigationController {

  public static void handleRedirectAndErrors(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    String action = request.getParameter("action");

    if ("external-docs".equals(action)) {
      // 重定向至外部链接(注意:调用后必须显式 return 阻断后续代码)
      response.sendRedirect("https://jakarta.ee/specifications/servlet/");
      return;
    }

    if ("forbidden-resource".equals(action)) {
      // 发送 403 错误并交由容器渲染错误页
      response.sendError(HttpServletResponse.SC_FORBIDDEN, "访问受限:无权操作核心系统资源");
      return;
    }

    // 正常业务流程
    response.setStatus(HttpServletResponse.SC_OK);
    response.setContentType("text/plain;charset=UTF-8");
    response.getWriter().write("请求已通过导航校验");
  }
}
  • void addCookie():(Cookie cookie),添加客户端 Cookie。将指定的 Cookie 附加至 HTTP 响应头中(生成 Set-Cookie 标头),客户端接收后将在符合域名与路径规则的后续请求中自动回传该 Cookie。

注意事项:

  1. addCookie() 可被多次调用,每次调用都会向响应报文追加一个独立的 Set-Cookie 标头,而不是相互覆盖。
  2. 安全属性设置:对于包含敏感会话信息的 Cookie,必须设置 setHttpOnly(true)(防御 XSS 攻击读取)以及 setSecure(true)(限制仅在 HTTPS 下传输)。在较新的 Servlet 规范与主流容器中,还需配合配置 SameSite 属性以防止跨站请求伪造(CSRF)。
  3. 时机限制:与设置响应头相同,addCookie() 必须在响应被正式提交(isCommitted() == false)前调用,否则添加的 Cookie 将无法输出至客户端。
java
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;

public class CookieManagementHelper {

  public static void issueAuthenticationCookie(HttpServletResponse response, String token) {
    // 创建指定键值的 Cookie 对象
    Cookie authCookie = new Cookie("AUTH_TOKEN", token);

    // 配置生命周期:7 天(单位:秒);0 表示立即删除,-1 表示内存级临时会话
    authCookie.setMaxAge(7 * 24 * 60 * 60);

    // 作用域限定:仅对当前应用根路径及子路径生效
    authCookie.setPath("/");

    // 安全防护:禁止浏览器 JavaScript 脚本读取,仅在网络传输时暴露
    authCookie.setHttpOnly(true);

    // 传输安全:仅在 TLS/HTTPS 加密通道中传输
    authCookie.setSecure(true);

    // 将 Cookie 注入到响应对象中
    response.addCookie(authCookie);
  }
}

URL 重写机制 ​

  • String encodeURL():(String url),普通链接会话编码。针对页面内超链接或表单提交的目标 URL,由容器自动判断客户端是否支持 Cookie;若不支持或会话刚建立,则在 URL 尾部追加 ;jsessionid=... 会话编码。

  • String encodeRedirectURL():(String url),重定向链接会话编码。专门针对即将传入 response.sendRedirect() 的目标 URL 进行会话编码判定与改写。

注意事项:

  1. 原有的 encodeUrl(String url) 与 encodeRedirectUrl(String url) 命名不符合 Java 驼峰规范,已被废弃,必须使用大写的 encodeURL() 与 encodeRedirectURL()。
  2. encodeURL() 具有智能判定能力:如果客户端请求本身已经携带了合法的 Cookie,容器会认为客户端具备 Cookie 存储能力,此时该方法将直接原样返回传入的 URL,而不会追加冗余的 ;jsessionid。
  3. 重定向与页面跳转时,若使用了无 Cookie 会话方案,所有涉及页面跳转的 URL 必须全部经过 encodeURL() 处理,否则只要出现一次漏转,客户端会话跟踪链就会彻底断裂。
java
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;

public class UrlRewriterHelper {

  public static String buildSessionAwareLink(HttpServletRequest request, HttpServletResponse response) {
    // 确保 Session 已建立
    HttpSession session = request.getSession(true);
    session.setAttribute("USER_STATUS", "ACTIVE");

    String targetPath = request.getContextPath() + "/user/profile";

    // 如果客户端禁用了 Cookie,encodeURL 会将其转换为: /context/user/profile;jsessionid=...
    return response.encodeURL(targetPath);
  }

  public static void redirectToProtectedPage(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    String redirectTarget = request.getContextPath() + "/dashboard";

    // 重定向专用编码判定
    String safeRedirectUrl = response.encodeRedirectURL(redirectTarget);
    response.sendRedirect(safeRedirectUrl);
  }
}

HTTP/2 协议拓展 ​

  • PushBuilder newPushBuilder():(),创建服务器推送构建器。在基于 HTTP/2 协议且客户端未禁用 Server Push 的连接上生成构建器对象;若底层为 HTTP/1.1 或连接不支持,则返回 null。

注意事项:

  1. newPushBuilder() 是 Servlet 4.0(Java EE 8 / Jakarta EE 8+)引入的特性。
  2. 在调用该方法前必须进行非空检查。因为当客户端通过 HTTP/1.1 访问、或在 HTTP/2 设置帧中显式声明 SETTINGS_ENABLE_PUSH=0 时,该方法必定返回 null。
  3. 行业规范演进提醒:虽然 Servlet 规范完整支持该接口,但由于现代前端构建体系(如资源哈希与缓存预热)成熟,且主流浏览器自 2022 年起已逐步停止对 HTTP/2 Server Push 的支持(推荐改用 103 Early Hints),在生产系统中使用时需充分评估客户端网络环境。
java
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.PushBuilder;

public class Http2PushHelper {

  public static void pushCriticalAssets(HttpServletRequest request, HttpServletResponse response) {
    // 获取 HTTP/2 服务端推送构建器
    PushBuilder pushBuilder = response.newPushBuilder();

    // 严格判空:若当前通道不支持或为 HTTP/1.1,则优雅降级
    if (pushBuilder != null) {
      pushBuilder.path("static/css/theme-dark.css")
             .addHeader("content-type", "text/css")
             .push();

      pushBuilder.path("static/js/bundle-core.js")
             .addHeader("content-type", "application/javascript")
             .push();

      System.out.println("HTTP/2 Server Push 资源宣告已派发");
    }
  }
}